﻿using System;
using System.Security.Cryptography;
using System.Text;


namespace WebPattern.Core.Security
{

    /// <summary>
    /// Application user roles.
    ///       Each role has access level, Sysadmin = 0, ..., RestrictedUser = 4
    /// </summary>
    /// <remarks>Each role has access level, Sysadmin = 0, ..., RestrictedUser = 4</remarks>
    public enum AppRoles : byte
    {
        /// <summary>
        /// Unlimited user (all-powerful)
        /// </summary>
        Sysadmin = 0,
        /// <summary>
        ///  Manage everything
        /// </summary>
        Admin = 1,
        /// <summary>
        /// Special user access
        /// </summary>
        Member = 2,
        /// <summary>
        /// Simple usrer access
        /// </summary>
        User = 3,
        /// <summary>
        /// Limited user access
        /// </summary>
        RestrictedUser = 4
    }
    
    public class SecurityHelper
    {
        #region Helper Class
        private class SaltedHash
        {
            HashAlgorithm HashProvider;
            int SalthLength;

            /// <summary>
            /// The constructor takes a HashAlgorithm as a parameter.
            /// </summary>
            /// <param name="HashAlgorithm">
            /// A <see cref="HashAlgorithm"/> HashAlgorihm which is derived from HashAlgorithm. C# provides
            /// the following classes: SHA1Managed,SHA256Managed, SHA384Managed, SHA512Managed and MD5CryptoServiceProvider
            /// </param>
            public SaltedHash(HashAlgorithm HashAlgorithm, int theSaltLength)
            {
                HashProvider = HashAlgorithm;
                SalthLength = theSaltLength;
            }

            /// <summary>
            /// Default constructor which initialises the SaltedHash with the SHA256Managed algorithm
            /// and a Salt of 4 bytes ( or 4*8 = 32 bits)
            /// </summary>
            public SaltedHash()
                : this(new SHA256Managed(), 10)
            {
            }

            /// <summary>
            /// The actual hash calculation is shared by both GetHashAndSalt and the VerifyHash functions
            /// </summary>
            /// <param name="Data">A byte array of the Data to Hash</param>
            /// <param name="Salt">A byte array of the Salt to add to the Hash</param>
            /// <returns>A byte array with the calculated hash</returns>
            private byte[] ComputeHash(byte[] Data, byte[] Salt)
            {
                // Allocate memory to store both the Data and Salt together
                byte[] DataAndSalt = new byte[Data.Length + SalthLength];

                // Copy both the data and salt into the new array
                Array.Copy(Data, DataAndSalt, Data.Length);
                Array.Copy(Salt, 0, DataAndSalt, Data.Length, SalthLength);

                // Calculate the hash
                // Compute hash value of our plain text with appended salt.
                return HashProvider.ComputeHash(DataAndSalt);
            }

            /// <summary>
            /// Given a data block this routine returns both a Hash and a Salt
            /// </summary>
            /// <param name="Data">
            /// A <see cref="System.Byte"/>byte array containing the data from which to derive the salt
            /// </param>
            /// <param name="Hash">
            /// A <see cref="System.Byte"/>byte array which will contain the hash calculated
            /// </param>
            /// <param name="Salt">
            /// A <see cref="System.Byte"/>byte array which will contain the salt generated
            /// </param>
            public void GetHashAndSalt(byte[] Data, out byte[] Hash, out byte[] Salt)
            {
                // Allocate memory for the salt
                Salt = new byte[SalthLength];

                // Strong runtime pseudo-random number generator, on Windows uses CryptAPI
                // on Unix /dev/urandom
                RNGCryptoServiceProvider random = new RNGCryptoServiceProvider();

                // Create a random salt
                random.GetNonZeroBytes(Salt);

                // Compute hash value of our data with the salt.
                Hash = ComputeHash(Data, Salt);
            }

            /// <summary>
            /// The routine provides a wrapper around the GetHashAndSalt function providing conversion
            /// from the required byte arrays to strings. Both the Hash and Salt are returned as Base-64 encoded strings.
            /// </summary>
            /// <param name="Data">
            /// A <see cref="System.String"/> string containing the data to hash
            /// </param>
            /// <param name="Hash">
            /// A <see cref="System.String"/> base64 encoded string containing the generated hash
            /// </param>
            /// <param name="Salt">
            /// A <see cref="System.String"/> base64 encoded string containing the generated salt
            /// </param>
            public void GetHashString(string Data, string Salt, out string Hash)
            {
                byte[] HashOut;
                byte[] SaltOut;
                byte[] _salt = GetSaltFromAnyString(Salt);

                // Obtain the Hash and Salt for the given string
                //GetHashAndSalt(Encoding.UTF8.GetBytes(Data), out HashOut, out SaltOut);
                HashOut = ComputeHash(Encoding.UTF8.GetBytes(Data), _salt);
                // Transform the byte[] to Base-64 encoded strings
                Hash = Convert.ToBase64String(HashOut);
                //Salt = Convert.ToBase64String(SaltOut);
            }

            /// <summary>
            /// This routine verifies whether the data generates the same hash as we had stored previously
            /// </summary>
            /// <param name="Data">The data to verify </param>
            /// <param name="Hash">The hash we had stored previously</param>
            /// <param name="Salt">The salt we had stored previously</param>
            /// <returns>True on a succesfull match</returns>
            public bool VerifyHash(byte[] Data, byte[] Hash, byte[] Salt)
            {
                byte[] NewHash = ComputeHash(Data, Salt);

                //  No easy array comparison in C# -- we do the legwork
                if (NewHash.Length != Hash.Length) return false;

                for (int Lp = 0; Lp < Hash.Length; Lp++)
                    if (!Hash[Lp].Equals(NewHash[Lp]))
                        return false;

                return true;
            }

            /// <summary>
            /// This routine provides a wrapper around VerifyHash converting the strings containing the
            /// data, hash and salt into byte arrays before calling VerifyHash.
            /// </summary>
            /// <param name="Data">A UTF-8 encoded string containing the data to verify</param>
            /// <param name="Hash">A base-64 encoded string containing the previously stored hash</param>
            /// <param name="Salt">A base-64 encoded string containing the previously stored salt</param>
            /// <returns></returns>
            public bool VerifyHashString(string Data, string Hash, string Salt)
            {
                byte[] HashToVerify = Convert.FromBase64String(Hash);
                byte[] SaltToVerify = Convert.FromBase64String(Salt);
                byte[] DataToVerify = Encoding.UTF8.GetBytes(Data);
                return VerifyHash(DataToVerify, HashToVerify, SaltToVerify);
            }

            /// <summary>
            /// Gets the salt.
            /// </summary>
            /// <param name="salt">The salt.</param>
            /// <returns></returns>
            /// <remarks></remarks>
            public byte[] GetSalt(string salt)
            {
                byte[] rVal = new byte[salt.Length];

                for (int i = 0; i < salt.Length; i++)
                {
                    rVal[i] = Convert.ToByte(salt[i]);
                }

                return rVal;
            }

            /// <summary>
            /// Gets the salt from username.
            /// </summary>
            /// <param name="username">The username.</param>
            /// <returns></returns>
            /// <remarks></remarks>
            public byte[] GetSaltFromAnyString(string salt)
            {
                if (salt.Length > 10)
                {
                    this.SalthLength = 10;
                    return GetSalt(salt.Remove(10, salt.Length - 10));
                }
                else
                {
                    this.SalthLength = salt.Length;
                    return GetSalt(salt); 
                }
            }
        }

        /// <summary>
        /// This little demo code shows how to encode a users password.
        /// </summary>

        private class SaltedHashDemo
        {
            public static void Main(string[] args)
            {
                // We use the default SHA-256 & 4 byte length
                SaltedHash demo = new SaltedHash();

                // We have a password, which will generate a Hash and Salt
                string Password = "MyGlook234";
                string Hash;
                string Salt = "Salt";

                demo.GetHashString(Password, Salt, out Hash);
                Console.WriteLine("Password = {0} , Hash = {1} , Salt = {2}", Password, Hash, Salt);

                // Password validation
                //
                // We need to pass both the earlier calculated Hash and Salt (we need to store this somewhere safe between sessions)

                // First check if a wrong password passes
                string WrongPassword = "OopsOops";
                Console.WriteLine("Verifying {0} = {1}", WrongPassword, demo.VerifyHashString(WrongPassword, Hash, Salt));

                // Check if the correct password passes
                Console.WriteLine("Verifying {0} = {1}", Password, demo.VerifyHashString(Password, Hash, Salt));

            }
        }
        #endregion

        #region Public Static Methods
        /// <summary>
        /// Passwords the encrypt.
        /// </summary>
        /// <param name="password">The password.</param>
        /// <param name="username">The username.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        public static string PasswordEncrypt(string password, string username)
        {
            if (string.IsNullOrWhiteSpace(username))
                throw new ArgumentException("username");

            if (string.IsNullOrWhiteSpace(password))
                throw new ArgumentException("password");

            string encryptedPassword;
            (new SaltedHash()).GetHashString(password, username, out encryptedPassword);
            return encryptedPassword;
        }

        /// <summary>
        /// Passwords the validator.
        /// </summary>
        /// <param name="password">The password.</param>
        /// <param name="username">The username.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        public static bool PasswordValidator(string password, string username)
        {
            if (string.IsNullOrWhiteSpace(username))
                throw new ArgumentException("username");

            if (string.IsNullOrWhiteSpace(password))
                throw new ArgumentException("password");

            UserRepository uRep = new UserRepository();
            string _encryptedPassword = uRep.GetPassword(username);

            string _inEncryptedPassword = PasswordEncrypt(password, username);

            return (_encryptedPassword == _inEncryptedPassword);
        }

        /// <summary>
        /// Passwords the compare.
        /// </summary>
        /// <param name="username">The username.</param>
        /// <param name="decriptedPassword">The decripted password.</param>
        /// <param name="encryptedPassword">The encrypted password.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        public static bool PasswordCompare(string username, string decriptedPassword, string encryptedPassword)
        {
            return (PasswordEncrypt(decriptedPassword, username) == encryptedPassword);
        }

        /// <summary>
        /// Passwords the decrypt.
        /// </summary>
        /// <param name="username">The username.</param>
        /// <param name="encryptedPassword">The encrypted password.</param>
        /// <returns></returns>
        /// <remarks></remarks>
        public static string PasswordDecrypt(string username, string encryptedPassword)
        {
            return "to do";
        }
        #endregion
    }
}